// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.net.test;

import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.chromium.base.Log;

import java.io.IOException;
import java.net.Socket;

/** A base class for simple HTTP test servers. */
public abstract class BaseHttpTestServer extends BaseTcpTestServer {
    private static final String TAG = "BaseHttpTestServer";

    /**
     * HttpResponseCallback owns the {@link DefaultHttpServerConnection} from a connection and
     * is responsible to send a response back over that connection when the
     * {@link #onResponse(HttpResponse)} callback is invoked.
     */
    public static class HttpResponseCallback {
        /**
         * The connection to the client.
         */
        private final DefaultHttpServerConnection mConn;

        /**
         * Creates a callback for the given connection.
         *
         * When the server is finished handling the request, it should invoke
         * {@link #onResponse(HttpResponse)} with the result.
         *
         * @param conn The connection to create a callback for.
         */
        public HttpResponseCallback(DefaultHttpServerConnection conn) {
            mConn = conn;
        }

        /**
         * Triggers sending the given response to the connected client.
         *
         * @param response the response to send over the connection.
         */
        public final void onResponse(HttpResponse response) {
            try {
                mConn.sendResponseHeader(response);
                mConn.sendResponseEntity(response);
            } catch (HttpException | IOException e) {
                Log.e(TAG, "Error while handling HTTP request", e);
            } finally {
                try {
                    mConn.close();
                } catch (IOException e) {
                    Log.e(TAG, "Error while closing socket for HTTP request", e);
                }
            }
        }
    }

    /**
     * Create an HTTP test server on the given port.
     *
     * @param serverPort The port to listen on for incoming HTTP connections.
     * @param acceptTimeoutMs The timeout for calls to ServerSocket.accept(), in milliseconds.
     * @throws IOException If the server port can't be bound.
     */
    public BaseHttpTestServer(int serverPort, int acceptTimeoutMs) throws IOException {
        super(serverPort, acceptTimeoutMs);
    }

    /**
     * Handle an incoming connection on |sock|.
     *
     * This will bind the socket, receive the request header (and entity, if applicable),
     * dispatch the request to a handler based on the request method, and send the response
     * generated by the handler.
     *
     * @param sock The socket for the incoming connection.
     * @throws IOException If an error occurs while reading from or writing to the socket.
     */
    @Override
    protected final void handle(Socket sock) throws IOException {
        HttpParams httpParams = getConnectionParams();
        DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
        conn.bind(sock, httpParams);

        HttpRequest request;
        try {
            request = conn.receiveRequestHeader();
            if (request instanceof HttpEntityEnclosingRequest) {
                conn.receiveRequestEntity((HttpEntityEnclosingRequest) request);
            }
        } catch (HttpException e) {
            Log.e(TAG, "Error while handling HTTP request", e);
            try {
                conn.close();
            } catch (IOException ioe) {
                Log.e(TAG, "Error while closing socket for HTTP request", ioe);
            }
            return;
        }
        handleRequest(conn, request);
    }

    private void handleRequest(DefaultHttpServerConnection conn, HttpRequest request) {
        HttpResponseCallback callback = new HttpResponseCallback(conn);
        try {
            switch (request.getRequestLine().getMethod()) {
                case HttpGet.METHOD_NAME:
                    handleGet(request, callback);
                    return;
                case HttpPost.METHOD_NAME:
                    handlePost((HttpEntityEnclosingRequest) request, callback);
                    return;
                default:
                    handleUnsupported(request, callback);
                    return;
            }
        } catch (HttpException e) {
            Log.e(TAG, "Error while handling HTTP request. Closing connection.", e);
            try {
                conn.close();
            } catch (IOException ioe) {
                Log.e(TAG, "Error while closing socket for HTTP request", ioe);
            }
        }
    }

    /**
     * Returns the parameters used to establish the HTTP connection.
     *
     * The default implementation returns a default BasicHttpParams instance.
     *
     * @return The HttpParams to use to establish the HTTP connection.
     */
    protected HttpParams getConnectionParams() {
        return new BasicHttpParams();
    }

    /**
     * Handle a GET request.
     *
     * The default implementation returns a 405. Override this function if you want to support
     * GET.
     *
     * Classes overriding this method must not call
     * {@link HttpResponseCallback#onResponse(HttpResponse)} before throwing the exception.
     *
     * @param request The GET request to handle.
     * @param callback The callback to give the response to the GET request.
     */
    protected void handleGet(HttpRequest request, HttpResponseCallback callback)
            throws HttpException {
        handleUnsupported(request, callback);
    }

    /**
     * Handle a POST request.
     *
     * The default implementation returns a 405. Override this function if you want to support
     * POST.
     *
     * Classes overriding this method must not call
     * {@link HttpResponseCallback#onResponse(HttpResponse)} before throwing the exception.
     *
     * @param request The POST request to handle.
     * @param callback The callback to give the response to the POST request.
     */
    protected void handlePost(HttpEntityEnclosingRequest request, HttpResponseCallback callback)
            throws HttpException {
        handleUnsupported(request, callback);
    }

    /** Handle an unsupported HTTP request.
     *
     * @param request The unsupported HTTP request.
     * @param callback The callback to give a 405 Method Not Allowed response.
     */
    protected void handleUnsupported(HttpRequest request, HttpResponseCallback callback) {
        callback.onResponse(
                new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_METHOD_NOT_ALLOWED, ""));
    }
}
